package com.mikesamuel.dateseries.shared;

import com.google.gwt.user.client.rpc.IsSerializable;

/** A day in a proleptic Gregorian calendar. */
public final class Day implements IsSerializable, Comparable<Day> {
  private int year;
  private int month, day;

  /**
   * Constructs a normalized date by rolling the input fields forward or back
   * as appropriate until they match a valid day.
   * For example, given (1975, 12, 32) will roll the month forward subtracting
   * the number of days in December to get (1975, 13, 1) and then roll the year
   * forward subtracting the number of months in a year to get 1976-1-1.
   */
  public Day(int year, int month, int day) {
    while (true) {
      if (month > 12) {
        month -= 12;
        year += 1;
      } else if (month <= 0) {
        month += 12;
        year -= 1;
      } else if (day <= 0) {
        if (month == 1) {
          day += 31;
          month = 12;
          year -= 1;
        } else {
          month -= 1;
          day += DateUtil.daysInMonth(year, month);
        }
      } else if (day > 28) {
        int n = DateUtil.daysInMonth(year, month);
        if (day > n) {
          day -= n;
          month += 1;
        } else {
          break;
        }
      } else {
        break;
      }
    }
    assert 1 <= month && month <= 12;
    assert 1 <= day && day <= DateUtil.daysInMonth(year, month);
    this.year = year;
    this.month = month;
    this.day = day;
  }

  Day() {
    // For GWT Serialization
    this.year = 0;
    this.month = this.day = 1;
  }

  public Day withDay(int day) {
    if (day == this.day) { return this; }
    return new Day(year, month, day);
  }

  /** Year y >= 1 is y CE/AD, year  y <= 0 is (y+1) BCE/BC. */
  public int year() { return year; }
  /** The month in the year where 1=January, ..., 12=December. */
  public int month() { return month; }
  /** The day of the month in [1, 31]. */
  public int day() { return day; }

  private static final WeekDay[] FIRST_SEVEN_WEEKDAYS = WeekDay.values();

  public WeekDay dayOfWeek() {
    return FIRST_SEVEN_WEEKDAYS[DateUtil.fixedFromGregorian(this) % 7];
  }

  /** yyyy-mm-dd. */
  @Override
  public String toString() {
    return year
      + (month < 10 ? "-0" : "-") + month
      + (day < 10 ? "-0" : "-") + day;
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof Day)) { return false; }
    Day that = (Day) o;
    return day == that.day && month == that.month && year == that.year;
  }

  @Override
  public int hashCode() {
    return day + 31 * (month + 12 * year);
  }

  @Override
  public int compareTo(Day d) {
    int delta = cmp(year, d.year);
    if (delta != 0) { return delta; }
    delta = cmp(month, d.month);
    if (delta != 0) { return delta; }
    return cmp(day, d.day);
  }

  private static final int cmp(int a, int b) {
    return a == b ? 0 : a < b ? -1 : 1;
  }
}
